package cn.parus.commons.utils;

import cn.parus.commons.common.Execption.CacheSerializationException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * @Author: Axel
 * @Description: TODO 通用缓存方法
 **/
@Slf4j
@Component
public class GenericCacheUtil {

    private final ObjectMapper objectMapper;
    private final GenericRedisUtil genericRedisUtil;
    private final RedisTemplate redisTemplate;

    @Autowired
    public GenericCacheUtil(GenericRedisUtil genericRedisUtil, RedisTemplate redisTemplate, ObjectMapper objectMapper) {
        this.genericRedisUtil = genericRedisUtil;
        this.redisTemplate = redisTemplate;
        this.objectMapper = objectMapper;
    }

    /**
     * 普通方法缓存
     * @param key
     * @param clazz
     * @param loader
     * @param timeout
     * @param unit
     * @return
     * @param <T>
     */
    public <T> T cacheable(String key,
                           Class<T> clazz,
                           Supplier<T> loader,
                           long timeout,
                           TimeUnit unit) {
        // 先查缓存
        T cached = genericRedisUtil.get(key, clazz);
        if (cached != null) {
            return cached;
        }
        // 如果缓存中为空，则查询数据库
        T loaded = loader.get();
        /**
         * redis穿透：已经设置缓存有效时间
         * TODO redis击穿：(待解决)
         */
        if (loaded != null) {
            try {
                String value = objectMapper.writeValueAsString(loaded);
                if (value == null || value.isEmpty() || value.equals("[]")) {
                    log.warn("发生redis穿透");
                    // 当出现缓存穿透时，设置value为null，并且过期时间为30s
                    genericRedisUtil.set(key, null, 30,TimeUnit.SECONDS);
                    redisTemplate.expire(key, 30, TimeUnit.SECONDS);
                } else {
                    redisTemplate.opsForValue().set(key, value, timeout, unit);
                    redisTemplate.expire(key, timeout, unit);
                }
            } catch (Exception e) {
                throw new CacheSerializationException("序列化失败", e);
            }
        }
        return loaded;
    }

    /**
     * 使用typerReference缓存
     * 用匿名函数修复泛型擦除
     * @param key
     * @param typeRef
     * @param loader
     * @param timeout
     * @param unit
     * @return
     * @param <T>
     */
    public <T> T cacheable(String key,
                           TypeReference<T> typeRef,
                           Supplier<T> loader,
                           long timeout,
                           TimeUnit unit) {
        String json = (String) redisTemplate.opsForValue().get(key);
        if (json != null) {
            try {
                return objectMapper.readValue(json, typeRef);
            }  catch (Exception e) {
                throw new CacheSerializationException("反序列化失败", e);
            }
        }
        /**
         * redis穿透：已经设置缓存有效时间
         * TODO redis击穿：(待解决)
         */
        T loaded = loader.get();
        if (loaded != null) {
            try {
                String value = objectMapper.writeValueAsString(loaded);
                if (value == null || value.isEmpty() || value.equals("[]")) {
                    log.warn("发生redis穿透");
                    // 当出现缓存穿透时，设置value为null，并且过期时间为30s
                    genericRedisUtil.set(key, null, 30,TimeUnit.SECONDS);
                    redisTemplate.expire(key, 30, TimeUnit.SECONDS);
                } else {
                    redisTemplate.opsForValue().set(key, value, timeout, unit);
                    redisTemplate.expire(key, timeout, unit);
                }
            } catch (Exception e) {
                throw new CacheSerializationException("序列化失败", e);
            }
        }
        return loaded;
    }

    /**
     * hash缓存
     * @param key
     * @param field
     * @param clazz
     * @param loader
     * @param timeout
     * @param unit
     * @return
     * @param <T>
     */
    public <T> T hcacheable(String key,
                           String field,
                           Class<T> clazz,
                           Supplier<T> loader,
                           long timeout,
                           TimeUnit unit) {
        // 先查缓存
        T cached = genericRedisUtil.hget(key, field, clazz);
        if (cached != null) {
            return cached;
        }
        // 如果缓存中为空，则查询数据库
        T loaded = loader.get();
        /**
         * redis穿透：已经设置缓存有效时间
         * TODO redis击穿：(待解决)
         */
        if (loaded != null) {
            try {
                String value = objectMapper.writeValueAsString(loaded);
                if(value==null || value.isEmpty() || value.equals("[]")) {
                    log.warn("redis穿透保护");
                    // 当出现缓存穿透时，设置value为null，并且过期时间为30s
                    genericRedisUtil.hset(key, null, TimeUnit.SECONDS);
                    redisTemplate.expire(key, 30, TimeUnit.SECONDS);
                } else {
                    redisTemplate.opsForHash().put(key, field, value);
                    redisTemplate.expire(key, timeout, unit);
                }
            } catch (Exception e) {
                throw new CacheSerializationException("序列化失败", e);
            }
        }
        return loaded;
    }

    /**
     * 使用typerReference缓存
     * 用匿名函数修复泛型擦除
     * @param key
     * @param field
     * @param typeRef
     * @param loader
     * @param timeout
     * @param unit
     * @return
     * @param <T>
     */
    public <T> T hcacheable(String key,
                            String field,
                           TypeReference<T> typeRef,
                           Supplier<T> loader,
                           long timeout,
                           TimeUnit unit) {
        String json = (String) redisTemplate.opsForHash().get(key, field);
        if (json != null) {
            try {
                return objectMapper.readValue(json, typeRef);
            }  catch (Exception e) {
                throw new CacheSerializationException("反序列化失败", e);
            }
        }
        /**
         * redis穿透：已经设置缓存有效时间
         * TODO redis击穿：(待解决)
         */
        T loaded = loader.get();
        if (loaded != null) {
            try {
                String value = objectMapper.writeValueAsString(loaded);
                if(value==null || value.isEmpty() || value.equals("[]")) {
                    log.warn("redis穿透保护");
                    // 当出现缓存穿透时，设置value为null，并且过期时间为30s
                    genericRedisUtil.hset(key, null, TimeUnit.SECONDS);
                    redisTemplate.expire(key, 30, TimeUnit.SECONDS);
                } else {
                    redisTemplate.opsForHash().put(key, field, value);
                    redisTemplate.expire(key, timeout, unit);
                }
            } catch (Exception e) {
                throw new CacheSerializationException("序列化失败", e);
            }
        }
        return loaded;
    }
}
